﻿using System;
using static SharpCompress.Compressors.Rar.UnpackV2017.Unpack.Unpack15Local;

namespace SharpCompress.Compressors.Rar.UnpackV2017;

internal partial class Unpack
{
    private const int STARTL1 = 2;

    private static readonly uint[] DecL1 =
    {
        0x8000,
        0xa000,
        0xc000,
        0xd000,
        0xe000,
        0xea00,
        0xee00,
        0xf000,
        0xf200,
        0xf200,
        0xffff,
    };

    private static readonly uint[] PosL1 = { 0, 0, 0, 2, 3, 5, 7, 11, 16, 20, 24, 32, 32 };

    private const int STARTL2 = 3;

    private static readonly uint[] DecL2 =
    {
        0xa000,
        0xc000,
        0xd000,
        0xe000,
        0xea00,
        0xee00,
        0xf000,
        0xf200,
        0xf240,
        0xffff,
    };

    private static readonly uint[] PosL2 = { 0, 0, 0, 0, 5, 7, 9, 13, 18, 22, 26, 34, 36 };

    private const int STARTHF0 = 4;

    private static readonly uint[] DecHf0 =
    {
        0x8000,
        0xc000,
        0xe000,
        0xf200,
        0xf200,
        0xf200,
        0xf200,
        0xf200,
        0xffff,
    };

    private static readonly uint[] PosHf0 = { 0, 0, 0, 0, 0, 8, 16, 24, 33, 33, 33, 33, 33 };

    private const int STARTHF1 = 5;

    private static readonly uint[] DecHf1 =
    {
        0x2000,
        0xc000,
        0xe000,
        0xf000,
        0xf200,
        0xf200,
        0xf7e0,
        0xffff,
    };

    private static readonly uint[] PosHf1 = { 0, 0, 0, 0, 0, 0, 4, 44, 60, 76, 80, 80, 127 };

    private const int STARTHF2 = 5;

    private static readonly uint[] DecHf2 =
    {
        0x1000,
        0x2400,
        0x8000,
        0xc000,
        0xfa00,
        0xffff,
        0xffff,
        0xffff,
    };

    private static readonly uint[] PosHf2 = { 0, 0, 0, 0, 0, 0, 2, 7, 53, 117, 233, 0, 0 };

    private const int STARTHF3 = 6;

    private static readonly uint[] DecHf3 =
    {
        0x800,
        0x2400,
        0xee00,
        0xfe80,
        0xffff,
        0xffff,
        0xffff,
    };

    private static readonly uint[] PosHf3 = { 0, 0, 0, 0, 0, 0, 0, 2, 16, 218, 251, 0, 0 };

    private const int STARTHF4 = 8;
    private static readonly uint[] DecHf4 = { 0xff00, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff };
    private static readonly uint[] PosHf4 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0 };

    private void Unpack15(bool Solid)
    {
        UnpInitData(Solid);
        UnpInitData15(Solid);
        UnpReadBuf();
        if (!Solid)
        {
            InitHuff();
            UnpPtr = 0;
        }
        else
        {
            UnpPtr = WrPtr;
        }

        --DestUnpSize;
        if (DestUnpSize >= 0)
        {
            GetFlagsBuf();
            FlagsCnt = 8;
        }

        while (DestUnpSize >= 0)
        {
            UnpPtr &= MaxWinMask;

            if (Inp.InAddr > ReadTop - 30 && !UnpReadBuf())
            {
                break;
            }

            if (((WrPtr - UnpPtr) & MaxWinMask) < 270 && WrPtr != UnpPtr)
            {
                UnpWriteBuf20();
            }

            if (StMode != 0)
            {
                HuffDecode();
                continue;
            }

            if (--FlagsCnt < 0)
            {
                GetFlagsBuf();
                FlagsCnt = 7;
            }

            if ((FlagBuf & 0x80) != 0)
            {
                FlagBuf <<= 1;
                if (Nlzb > Nhfb)
                {
                    LongLZ();
                }
                else
                {
                    HuffDecode();
                }
            }
            else
            {
                FlagBuf <<= 1;
                if (--FlagsCnt < 0)
                {
                    GetFlagsBuf();
                    FlagsCnt = 7;
                }
                if ((FlagBuf & 0x80) != 0)
                {
                    FlagBuf <<= 1;
                    if (Nlzb > Nhfb)
                    {
                        HuffDecode();
                    }
                    else
                    {
                        LongLZ();
                    }
                }
                else
                {
                    FlagBuf <<= 1;
                    ShortLZ();
                }
            }
        }
        UnpWriteBuf20();
    }

    private async System.Threading.Tasks.Task Unpack15Async(
        bool Solid,
        System.Threading.CancellationToken cancellationToken = default
    )
    {
        UnpInitData(Solid);
        UnpInitData15(Solid);
        await UnpReadBufAsync(cancellationToken).ConfigureAwait(false);
        if (!Solid)
        {
            InitHuff();
            UnpPtr = 0;
        }
        else
        {
            UnpPtr = WrPtr;
        }

        --DestUnpSize;
        if (DestUnpSize >= 0)
        {
            GetFlagsBuf();
            FlagsCnt = 8;
        }

        while (DestUnpSize >= 0)
        {
            UnpPtr &= MaxWinMask;

            if (
                Inp.InAddr > ReadTop - 30
                && !await UnpReadBufAsync(cancellationToken).ConfigureAwait(false)
            )
            {
                break;
            }

            if (((WrPtr - UnpPtr) & MaxWinMask) < 270 && WrPtr != UnpPtr)
            {
                await UnpWriteBuf20Async(cancellationToken).ConfigureAwait(false);
            }

            if (StMode != 0)
            {
                HuffDecode();
                continue;
            }

            if (--FlagsCnt < 0)
            {
                GetFlagsBuf();
                FlagsCnt = 7;
            }

            if ((FlagBuf & 0x80) != 0)
            {
                FlagBuf <<= 1;
                if (Nlzb > Nhfb)
                {
                    LongLZ();
                }
                else
                {
                    HuffDecode();
                }
            }
            else
            {
                FlagBuf <<= 1;
                if (--FlagsCnt < 0)
                {
                    GetFlagsBuf();
                    FlagsCnt = 7;
                }
                if ((FlagBuf & 0x80) != 0)
                {
                    FlagBuf <<= 1;
                    if (Nlzb > Nhfb)
                    {
                        HuffDecode();
                    }
                    else
                    {
                        LongLZ();
                    }
                }
                else
                {
                    FlagBuf <<= 1;
                    ShortLZ();
                }
            }
        }
        await UnpWriteBuf20Async(cancellationToken).ConfigureAwait(false);
    }

    //#define GetShortLen1(pos) ((pos)==1 ? Buf60+3:ShortLen1[pos])
    private uint GetShortLen1(uint pos) => ((pos) == 1 ? (uint)(Buf60 + 3) : ShortLen1[pos]);

    //#define GetShortLen2(pos) ((pos)==3 ? Buf60+3:ShortLen2[pos])
    private uint GetShortLen2(uint pos) => ((pos) == 3 ? (uint)(Buf60 + 3) : ShortLen2[pos]);

    internal static class Unpack15Local
    {
        public static readonly uint[] ShortLen1 =
        {
            1,
            3,
            4,
            4,
            5,
            6,
            7,
            8,
            8,
            4,
            4,
            5,
            6,
            6,
            4,
            0,
        };
        public static readonly uint[] ShortXor1 =
        {
            0,
            0xa0,
            0xd0,
            0xe0,
            0xf0,
            0xf8,
            0xfc,
            0xfe,
            0xff,
            0xc0,
            0x80,
            0x90,
            0x98,
            0x9c,
            0xb0,
        };
        public static readonly uint[] ShortLen2 =
        {
            2,
            3,
            3,
            3,
            4,
            4,
            5,
            6,
            6,
            4,
            4,
            5,
            6,
            6,
            4,
            0,
        };
        public static readonly uint[] ShortXor2 =
        {
            0,
            0x40,
            0x60,
            0xa0,
            0xd0,
            0xe0,
            0xf0,
            0xf8,
            0xfc,
            0xc0,
            0x80,
            0x90,
            0x98,
            0x9c,
            0xb0,
        };
    }

    private void ShortLZ()
    {
        uint Length,
            SaveLength;
        uint LastDistance;
        uint Distance;
        int DistancePlace;
        NumHuf = 0;

        var BitField = Inp.fgetbits();
        if (LCount == 2)
        {
            Inp.faddbits(1);
            if (BitField >= 0x8000)
            {
                CopyString15(LastDist, LastLength);
                return;
            }
            BitField <<= 1;
            LCount = 0;
        }

        BitField >>= 8;

        //  not thread safe, replaced by GetShortLen1 and GetShortLen2 macro
        //  ShortLen1[1]=ShortLen2[3]=Buf60+3;

        if (AvrLn1 < 37)
        {
            for (Length = 0; ; Length++)
            {
                if (((BitField ^ ShortXor1[Length]) & (~(0xff >> (int)GetShortLen1(Length)))) == 0)
                {
                    break;
                }
            }

            Inp.faddbits(GetShortLen1(Length));
        }
        else
        {
            for (Length = 0; ; Length++)
            {
                if (((BitField ^ ShortXor2[Length]) & (~(0xff >> (int)GetShortLen2(Length)))) == 0)
                {
                    break;
                }
            }

            Inp.faddbits(GetShortLen2(Length));
        }

        if (Length >= 9)
        {
            if (Length == 9)
            {
                LCount++;
                CopyString15(LastDist, LastLength);
                return;
            }
            if (Length == 14)
            {
                LCount = 0;
                Length = DecodeNum(Inp.fgetbits(), STARTL2, DecL2, PosL2) + 5;
                Distance = (Inp.fgetbits() >> 1) | 0x8000;
                Inp.faddbits(15);
                LastLength = Length;
                LastDist = Distance;
                CopyString15(Distance, Length);
                return;
            }

            LCount = 0;
            SaveLength = Length;
            Distance = OldDist[(OldDistPtr - (Length - 9)) & 3];
            Length = DecodeNum(Inp.fgetbits(), STARTL1, DecL1, PosL1) + 2;
            if (Length == 0x101 && SaveLength == 10)
            {
                Buf60 ^= 1;
                return;
            }
            if (Distance > 256)
            {
                Length++;
            }

            if (Distance >= MaxDist3)
            {
                Length++;
            }

            OldDist[OldDistPtr++] = Distance;
            OldDistPtr &= 3;
            LastLength = Length;
            LastDist = Distance;
            CopyString15(Distance, Length);
            return;
        }

        LCount = 0;
        AvrLn1 += Length;
        AvrLn1 -= AvrLn1 >> 4;

        DistancePlace = (int)(DecodeNum(Inp.fgetbits(), STARTHF2, DecHf2, PosHf2) & 0xff);
        Distance = ChSetA[DistancePlace];
        if (--DistancePlace != -1)
        {
            LastDistance = ChSetA[DistancePlace];
            ChSetA[DistancePlace + 1] = (ushort)LastDistance;
            ChSetA[DistancePlace] = (ushort)Distance;
        }
        Length += 2;
        OldDist[OldDistPtr++] = ++Distance;
        OldDistPtr &= 3;
        LastLength = Length;
        LastDist = Distance;
        CopyString15(Distance, Length);
    }

    private void LongLZ()
    {
        uint Length;
        uint Distance;
        uint DistancePlace,
            NewDistancePlace;
        uint OldAvr2,
            OldAvr3;

        NumHuf = 0;
        Nlzb += 16;
        if (Nlzb > 0xff)
        {
            Nlzb = 0x90;
            Nhfb >>= 1;
        }
        OldAvr2 = AvrLn2;

        var BitField = Inp.fgetbits();
        if (AvrLn2 >= 122)
        {
            Length = DecodeNum(BitField, STARTL2, DecL2, PosL2);
        }
        else if (AvrLn2 >= 64)
        {
            Length = DecodeNum(BitField, STARTL1, DecL1, PosL1);
        }
        else if (BitField < 0x100)
        {
            Length = BitField;
            Inp.faddbits(16);
        }
        else
        {
            for (Length = 0; ((BitField << (int)Length) & 0x8000) == 0; Length++)
            {
                ;
            }

            Inp.faddbits(Length + 1);
        }

        AvrLn2 += Length;
        AvrLn2 -= AvrLn2 >> 5;

        BitField = Inp.fgetbits();
        if (AvrPlcB > 0x28ff)
        {
            DistancePlace = DecodeNum(BitField, STARTHF2, DecHf2, PosHf2);
        }
        else if (AvrPlcB > 0x6ff)
        {
            DistancePlace = DecodeNum(BitField, STARTHF1, DecHf1, PosHf1);
        }
        else
        {
            DistancePlace = DecodeNum(BitField, STARTHF0, DecHf0, PosHf0);
        }

        AvrPlcB += DistancePlace;
        AvrPlcB -= AvrPlcB >> 8;
        while (true)
        {
            Distance = ChSetB[DistancePlace & 0xff];
            NewDistancePlace = NToPlB[Distance++ & 0xff]++;
            if ((Distance & 0xff) != 0)
            {
                CorrHuff(ChSetB, NToPlB);
            }
            else
            {
                break;
            }
        }

        ChSetB[DistancePlace & 0xff] = ChSetB[NewDistancePlace];
        ChSetB[NewDistancePlace] = (ushort)Distance;

        Distance = ((Distance & 0xff00) | (Inp.fgetbits() >> 8)) >> 1;
        Inp.faddbits(7);

        OldAvr3 = AvrLn3;
        if (Length != 1 && Length != 4)
        {
            if (Length == 0 && Distance <= MaxDist3)
            {
                AvrLn3++;
                AvrLn3 -= AvrLn3 >> 8;
            }
            else if (AvrLn3 > 0)
            {
                AvrLn3--;
            }
        }

        Length += 3;
        if (Distance >= MaxDist3)
        {
            Length++;
        }

        if (Distance <= 256)
        {
            Length += 8;
        }

        if (OldAvr3 > 0xb0 || AvrPlc >= 0x2a00 && OldAvr2 < 0x40)
        {
            MaxDist3 = 0x7f00;
        }
        else
        {
            MaxDist3 = 0x2001;
        }

        OldDist[OldDistPtr++] = Distance;
        OldDistPtr &= 3;
        LastLength = Length;
        LastDist = Distance;
        CopyString15(Distance, Length);
    }

    private void HuffDecode()
    {
        uint CurByte,
            NewBytePlace;
        uint Length;
        uint Distance;
        int BytePlace;

        var BitField = Inp.fgetbits();

        if (AvrPlc > 0x75ff)
        {
            BytePlace = (int)DecodeNum(BitField, STARTHF4, DecHf4, PosHf4);
        }
        else if (AvrPlc > 0x5dff)
        {
            BytePlace = (int)DecodeNum(BitField, STARTHF3, DecHf3, PosHf3);
        }
        else if (AvrPlc > 0x35ff)
        {
            BytePlace = (int)DecodeNum(BitField, STARTHF2, DecHf2, PosHf2);
        }
        else if (AvrPlc > 0x0dff)
        {
            BytePlace = (int)DecodeNum(BitField, STARTHF1, DecHf1, PosHf1);
        }
        else
        {
            BytePlace = (int)DecodeNum(BitField, STARTHF0, DecHf0, PosHf0);
        }

        BytePlace &= 0xff;
        if (StMode != 0)
        {
            if (BytePlace == 0 && BitField > 0xfff)
            {
                BytePlace = 0x100;
            }

            if (--BytePlace == -1)
            {
                BitField = Inp.fgetbits();
                Inp.faddbits(1);
                if ((BitField & 0x8000) != 0)
                {
                    NumHuf = StMode = 0;
                    return;
                }
                else
                {
                    Length = (BitField & 0x4000) != 0 ? 4U : 3;
                    Inp.faddbits(1);
                    Distance = DecodeNum(Inp.fgetbits(), STARTHF2, DecHf2, PosHf2);
                    Distance = (Distance << 5) | (Inp.fgetbits() >> 11);
                    Inp.faddbits(5);
                    CopyString15(Distance, Length);
                    return;
                }
            }
        }
        else if (NumHuf++ >= 16 && FlagsCnt == 0)
        {
            StMode = 1;
        }

        AvrPlc += (uint)BytePlace;
        AvrPlc -= AvrPlc >> 8;
        Nhfb += 16;
        if (Nhfb > 0xff)
        {
            Nhfb = 0x90;
            Nlzb >>= 1;
        }

        Window[UnpPtr++] = (byte)(ChSet[BytePlace] >> 8);
        --DestUnpSize;

        while (true)
        {
            CurByte = ChSet[BytePlace];
            NewBytePlace = NToPl[CurByte++ & 0xff]++;
            if ((CurByte & 0xff) > 0xa1)
            {
                CorrHuff(ChSet, NToPl);
            }
            else
            {
                break;
            }
        }

        ChSet[BytePlace] = ChSet[NewBytePlace];
        ChSet[NewBytePlace] = (ushort)CurByte;
    }

    private void GetFlagsBuf()
    {
        uint Flags,
            NewFlagsPlace;
        var FlagsPlace = DecodeNum(Inp.fgetbits(), STARTHF2, DecHf2, PosHf2);

        // Our Huffman table stores 257 items and needs all them in other parts
        // of code such as when StMode is on, so the first item is control item.
        // While normally we do not use the last item to code the flags byte here,
        // we need to check for value 256 when unpacking in case we unpack
        // a corrupt archive.
        if (FlagsPlace >= ChSetC.Length)
        {
            return;
        }

        while (true)
        {
            Flags = ChSetC[FlagsPlace];
            FlagBuf = Flags >> 8;
            NewFlagsPlace = NToPlC[Flags++ & 0xff]++;
            if ((Flags & 0xff) != 0)
            {
                break;
            }

            CorrHuff(ChSetC, NToPlC);
        }

        ChSetC[FlagsPlace] = ChSetC[NewFlagsPlace];
        ChSetC[NewFlagsPlace] = (ushort)Flags;
    }

    private void UnpInitData15(bool Solid)
    {
        if (!Solid)
        {
            AvrPlcB = AvrLn1 = AvrLn2 = AvrLn3 = 0;
            NumHuf = Buf60 = 0;
            AvrPlc = 0x3500;
            MaxDist3 = 0x2001;
            Nhfb = Nlzb = 0x80;
        }
        FlagsCnt = 0;
        FlagBuf = 0;
        StMode = 0;
        LCount = 0;
        ReadTop = 0;
    }

    private void InitHuff()
    {
        for (uint I = 0; I < 256; I++)
        {
            ChSet[I] = ChSetB[I] = (ushort)(I << 8);
            ChSetA[I] = (ushort)I;
            ChSetC[I] = (ushort)(((~I + 1) & 0xff) << 8);
        }
        new Span<byte>(NToPl).Clear();
        new Span<byte>(NToPlB).Clear();
        new Span<byte>(NToPlC).Clear();
        CorrHuff(ChSetB, NToPlB);
    }

    private void CorrHuff(ushort[] CharSet, byte[] NumToPlace)
    {
        int I,
            J;
        for (I = 7; I >= 0; I--)
        {
            for (J = 0; J < 32; J++)
            {
                CharSet[J] = (ushort)((CharSet[J] & ~0xff) | I);
            }
        }

        new Span<byte>(NumToPlace, 0, NToPl.Length).Clear();
        for (I = 6; I >= 0; I--)
        {
            NumToPlace[I] = (byte)((7 - I) * 32);
        }
    }

    private void CopyString15(uint Distance, uint Length)
    {
        DestUnpSize -= Length;
        while (Length-- != 0)
        {
            Window[UnpPtr] = Window[(UnpPtr - Distance) & MaxWinMask];
            UnpPtr = (UnpPtr + 1) & MaxWinMask;
        }
    }

    private uint DecodeNum(uint Num, uint StartPos, uint[] DecTab, uint[] PosTab)
    {
        int I;
        for (Num &= 0xfff0, I = 0; DecTab[I] <= Num; I++)
        {
            StartPos++;
        }

        Inp.faddbits(StartPos);
        return (((Num - (I != 0 ? DecTab[I - 1] : 0)) >> (int)(16 - StartPos)) + PosTab[StartPos]);
    }
}
